package cn.sc.summer.thread.aspect;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.sc.summer.thread.annotation.AfterCommit;
import cn.sc.summer.thread.annotation.BeforeCommit;
import cn.sc.summer.thread.util.TransactionalExecutor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
@Aspect
@EnableAspectJAutoProxy
public class TransactionalAop implements DisposableBean {

    @Resource
    private TransactionalExecutor transactionalExecutor;

    /**
     * 当前线程子线程
     */
    private static final ThreadLocal<Map<String, Boolean>> THREAD_LOCAL = new ThreadLocal<>();

    private static final Map<String, Boolean> COMMIT_MAP = new HashMap<>();

    @PostConstruct
    private void init() {
        COMMIT_MAP.put(TransactionalExecutor.BEFORE_COMMIT, Boolean.TRUE);
        COMMIT_MAP.put(TransactionalExecutor.AFTER_COMMIT, Boolean.TRUE);
    }


    @Pointcut("@annotation(cn.sc.summer.thread.annotation.AfterCommit)")
    public void pointCutAfter() {
    }

    @Pointcut("@annotation(cn.sc.summer.thread.annotation.BeforeCommit)")
    public void pointCutBefore() {
    }


    @Before(value = "pointCutAfter()")
    public void afterPointCut() {
        if (CollUtil.isEmpty(THREAD_LOCAL.get())) {
            THREAD_LOCAL.set(COMMIT_MAP);
        }
    }

    @AfterThrowing(value = "pointCutAfter()")
    public void afterThrowing1() {
        THREAD_LOCAL.get().put(TransactionalExecutor.AFTER_COMMIT, Boolean.FALSE);
    }


    @Before(value = "pointCutBefore()")
    public void beforePointCut() {
        if (CollUtil.isEmpty(THREAD_LOCAL.get())) {
            THREAD_LOCAL.set(COMMIT_MAP);
        }
    }

    @AfterThrowing(value = "pointCutBefore()")
    public void afterThrowing2() {
        THREAD_LOCAL.get().put(TransactionalExecutor.BEFORE_COMMIT, Boolean.FALSE);
    }


    @After("pointCutAfter()")
    public void afterPointCut(JoinPoint joinPoint) {
        if (THREAD_LOCAL.get().get(TransactionalExecutor.AFTER_COMMIT)) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            if (!method.isAnnotationPresent(AfterCommit.class)) {
                return;
            }
            AfterCommit afterCommit = method.getAnnotation(AfterCommit.class);
            String methodName = afterCommit.method();
            if (CharSequenceUtil.isBlank(methodName)) {
                return;
            }

            Class<?> declaringClass = method.getDeclaringClass();

            transactionalExecutor.afterRun(() -> {
                try {
                    Method declaredMethod = declaringClass.getDeclaredMethod(methodName);
                    declaredMethod.invoke(joinPoint.getTarget());
                } catch (Throwable e) {
                    throw new RuntimeException(e);
                }
            });

        }
    }

    @After("pointCutBefore()")
    public void beforePointCut(JoinPoint joinPoint) {
        if (THREAD_LOCAL.get().get(TransactionalExecutor.BEFORE_COMMIT)) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            if (!method.isAnnotationPresent(BeforeCommit.class)) {
                return;
            }
            BeforeCommit beforeCommit = method.getAnnotation(BeforeCommit.class);
            String methodName = beforeCommit.method();
            if (CharSequenceUtil.isBlank(methodName)) {
                return;
            }

            Class<?> declaringClass = method.getDeclaringClass();

            transactionalExecutor.beforeRun(() -> {
                try {
                    Method declaredMethod = declaringClass.getDeclaredMethod(methodName);
                    declaredMethod.invoke(joinPoint.getTarget());
                } catch (Throwable e) {
                    throw new RuntimeException(e);
                }
            });

        }
    }

    /**
     * Invoked by the containing {@code BeanFactory} on destruction of a bean.
     */
    @Override
    public void destroy() {
        THREAD_LOCAL.remove();
    }
}
